﻿using Microsoft.AspNetCore.DataProtection.KeyManagement;
using RocksDbSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace server
{
    internal class dbtool
    {
        public class Status
        {
            public UInt64 Height;
            public byte[] Chain_InitHash;
            public byte[] RootHash;
        }
        public class TxInfo
        {
            public UInt64 Height;
            public uint datalen;
            public byte[] RootHash;
            public TxInfo(byte[] data)
            {
                this.Height = BitConverter.ToUInt64(data, 0);
                this.datalen = BitConverter.ToUInt32(data, 8);
                RootHash = data.Skip(12).ToArray();
            }
            public TxInfo(UInt64 height, uint _len, byte[] roothash)
            {
                this.Height = height;
                this.datalen = _len;
                this.RootHash = roothash;
            }
            public byte[] ToData()
            {
                var hd = BitConverter.GetBytes(this.Height);
                var ld = BitConverter.GetBytes(this.datalen);

                return hd.Concat(ld).Concat(this.RootHash).ToArray();
            }
        }
        static RocksDb db;


        public static void SafeClear(Action<bool, string> result)
        {
            Cmd cmd = new Cmd();
            cmd.putdata = null;
            cmd.height = 0;//传0 表示clear
            cmd.putlasthash = null;
            cmd.onResult = result;
            cmds.Enqueue(cmd);
        }
        public static void SafePutTran(ulong height, byte[] rootHash, byte[] data, Action<bool, string> result)
        {
            Cmd cmd = new Cmd();
            cmd.putdata = data;
            cmd.height = height;
            cmd.putlasthash = rootHash;
            cmd.onResult = result;
            cmds.Enqueue(cmd);
        }

        class Cmd
        {
            public byte[] putdata;
            public byte[] putlasthash;
            public ulong height;
            public Action<bool, string> onResult;
        }
        static System.Collections.Concurrent.ConcurrentQueue<Cmd> cmds = new System.Collections.Concurrent.ConcurrentQueue<Cmd>();
        static void WriteThread()
        {
            while (true)
            {
                if (cmds.TryDequeue(out var c))
                {
                    try
                    {
                        Status s = GetStatus();
                        WriteBatch wb = new WriteBatch();
                        if (c.height == 0)
                        {
                            //Clear
                            wb.Put(Key_Height, BitConverter.GetBytes(0));
                            wb.Put(Key_RootHash, s.Chain_InitHash);
                        }
                        else
                        {

                            if (c.height != s.Height + 1)
                            {
                                if (c.onResult != null)
                                {
                                    c.onResult(false, "F1:高度不正确，不能addtran");
                                    return;
                                }
                                else
                                {
                                    throw new Exception("高度不正确，不能addtran");
                                }
                            }
                            if (HexTool.HexEqual(c.putlasthash, s.RootHash) == false)
                            {
                                if (c.onResult != null)
                                {
                                    c.onResult(false, "F2:RootHash不正确，不能addtran");
                                    return;
                                }
                                else
                                {
                                    throw new Exception("RootHash不正确，不能addtran");
                                }
                            }
                         
                            var txhash = HexTool.CalcTranHash(c.height, c.putlasthash, c.putdata);
                            var roothash = HexTool.CalcRootHash(s.RootHash, txhash);
                            var txinfo = new TxInfo(c.height, (uint)c.putdata.Length, roothash);

                            wb.Put(Key_Height, BitConverter.GetBytes(c.height));
                            wb.Put(Key_RootHash, roothash);
                            wb.Put(MakeKey_TranData(txhash), c.putdata);

                            wb.Put(MakeKey_TranInfo(txhash), txinfo.ToData());
                            wb.Put(MakeKey_TranID2Hash(c.height), txhash);
                        }

                        db.Write(wb);
                        lastSnap = db.CreateSnapshot();
                        if (c.onResult != null)
                        {
                            c.onResult(true, "succ");
                        }
                    }
                    catch (Exception err)
                    {
                        Console.WriteLine("DB Parse Error:" + err.ToString());

                        if (c.onResult != null)
                        {
                            c.onResult(false, err.Message);
                        }
                    }
                }
                else
                {
                    System.Threading.Thread.Sleep(1);
                }
            }
        }
        public static void Init(string inittag, string path)
        {

            DbOptions dbOptions = new DbOptions();
            dbOptions.SetCreateIfMissing(true);
            db = RocksDb.Open(dbOptions, path);
            lastSnap = db.CreateSnapshot();
            var hash = HexTool.CalcHash(Encoding.UTF8.GetBytes(inittag));
            Status s = GetStatus();
            if (s == null)
            {
                Console.WriteLine("Db Init.");

                db.Put(Key_Height, BitConverter.GetBytes(0));
                db.Put(Key_Chain_InitHash, hash);
                db.Put(Key_RootHash, hash);

                Console.WriteLine("Height=0");
                Console.WriteLine("Key_Chain_InitHash=" + HexTool.Hex2Str(hash));
                Console.WriteLine("RootHash=" + HexTool.Hex2Str(hash));
            }
            else
            {
                if (HexTool.HexEqual(hash, s.Chain_InitHash) == false)
                {
                    throw new Exception("Init hash 和既有数据库不符");
                }
                Console.WriteLine("Db Load.");
                Console.WriteLine("Height=" + s.Height);
                Console.WriteLine("Key_Chain_InitHash=" + HexTool.Hex2Str(s.Chain_InitHash));
                Console.WriteLine("RootHash=" + HexTool.Hex2Str(s.RootHash));

            }

            System.Threading.Thread t = new Thread(WriteThread);
            t.Name = "DBWriteWorker";
            t.IsBackground = true;
            t.Start();
        }
        static Snapshot lastSnap;
        [ThreadStatic]
        static ReadOptions readop;
        enum Tag : byte
        {
            Chain_InitHash = 1,
            Height = 2,
            RootHash = 3,
            TranID2Hash = 4,
            TranInfo = 5,
            TranData = 6,
        }
        public static Status GetStatus()
        {
            if (readop == null)
                readop = new ReadOptions();
            readop.SetSnapshot(lastSnap);
            var inithash = db.Get(Key_Chain_InitHash, null, readop);
            if (inithash == null || inithash.Length < 32)
                return null;

            var roothash = db.Get(Key_RootHash, null, readop);
            if (roothash == null || roothash.Length < 32)
                return null;

            var heightdata = db.Get(Key_Height, null, readop);
            if (heightdata == null || heightdata.Length < 4)
                return null;
            var height = heightdata.Length == 4 ? BitConverter.ToUInt32(heightdata) : BitConverter.ToUInt64(heightdata);
            return new Status() { Chain_InitHash = inithash, Height = height, RootHash = roothash };
        }

        public static TxInfo GetTxInfo(byte[] hash)
        {
            if (readop == null)
                readop = new ReadOptions();
            readop.SetSnapshot(lastSnap);
            var key = MakeKey_TranInfo(hash);
            var data = db.Get(key, null, readop);
            if (data == null || data.Length < 12)
                return null;
            return new TxInfo(data);
        }
        public static byte[] GetTxData(byte[] hash)
        {
            if (readop == null)
                readop = new ReadOptions();
            readop.SetSnapshot(lastSnap);
            var key = MakeKey_TranData(hash);
            var data = db.Get(key, null, readop);
            return data;
        }
        public static byte[] GetTXIDByHeight(UInt64 height)
        {
            if (readop == null)
                readop = new ReadOptions();
            readop.SetSnapshot(lastSnap);
            var key = MakeKey_TranID2Hash(height);
            var data = db.Get(key, null, readop);
            return data;

        }
        static readonly byte[] Key_Chain_InitHash = new byte[] { (byte)Tag.Chain_InitHash };
        static readonly byte[] Key_Height = new byte[] { (byte)Tag.Height };
        static readonly byte[] Key_RootHash = new byte[] { (byte)Tag.RootHash };

        static byte[] MakeKey_TranID2Hash(UInt64 ID)
        {
            var key = new byte[9];
            key[0] = (byte)Tag.TranID2Hash;
            return key.Concat(BitConverter.GetBytes(ID)).ToArray();
        }
        static byte[] MakeKey_TranData(byte[] hash)
        {
            var key = new byte[33];
            key[0] = (byte)Tag.TranData;
            return key.Concat(hash).ToArray();
        }
        static byte[] MakeKey_TranInfo(byte[] hash)
        {
            var key = new byte[33];
            key[0] = (byte)Tag.TranInfo;
            return key.Concat(hash).ToArray();
        }
    }
}
